#!/usr/bin/env python
#
# Rough password strength checker, based on entropy calculations.
# Will not store or reveal the password, as long as 'getpass' is available.
#
# What's up with the "passing grades" used in this program? Well, I'm not a 
# cryptographer, but much of the stuff I used comes from this security 
# researcher, who taught me how entropy and search space relate.
#
# I understand that stringing dictionary words or other easily-guessable 
# combinations can make brute-forcing easier than it seems, so there's a 
# warning for that. At any rate, I put out the grand entropy total out, so
# that you can draw up your own conclusions.
#
# TODO: currently, only ASCII passwords (passwords you can type in any 
#       keyboard) work. Any other character set will be ignored.
#
#    Copyright (C) 2017 - kzimmermann <https://quitter.se/kzimmermann>
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

import os
import sys
import math

try:
    from getpass import getpass

except ImportError:
    print "Error: getpass is not available in this system."
    print "Sorry, for the sake of privacy, we cannot proceed."
    sys.exit(1)

lowercase = "abcdefgefghijklmnopqrstuvwxyz"
uppercase = lowercase.upper()
digits = "0123456789"
nonalpha = "!@#$%^&*()_+=-{}[]'\\\":;/?.,<>`~ "

def test_set(string, set):
    '''
    Does the string has any of the characters available in this set?
    '''

    available = False
    for char in string:
        if char in set:
            available = True
            break
    return available
            

def entropy(string):
    '''
    What is the entropy per character in this string?

    Literally: H = N (log(P,2))
    '''

    has_lower = test_set(string, lowercase)
    has_upper = test_set(string, uppercase)
    has_digit = test_set(string, digits)
    has_other = test_set(string, nonalpha)
    
    return (math.log((
                has_lower * len(lowercase)
                + has_upper * len(uppercase)
                + has_digit * len(digits)
                + has_other * len(nonalpha)), 2)
                )

def combinate(string):
    return len(string) * entropy(string)

def test_ent():
    pw = "blabberm0uth!123"
    bits = entropy(pw)

    print "Password '%s' contains about %.2f bits of entropy per char, totalling about %.2f bits of entropy." % (pw, bits, combinate(pw))

def main():
    test_string = getpass("Enter your password (won't be shown): ")
    total = combinate(test_string)
    if total < 60.0:
        result = "very weak"
    elif total < 80.0:
        result = "weak" 
    elif total < 88.0: 
        result = "passable" 
    elif total < 94.0: 
        result = "pretty good"
    else:
        result = "very strong"

    print "Your password contains a total of about %.2f bits of entropy which is %s." % (total, result)

    if total < 80.0:
        print "More characters, and not character diversity, is usually the best way to strengthen a password. A password of 14 characters, digits and nonalphanumeric included, should be enough." 
        print "On the other hand, have you considered using a password manager?"
    elif total < 88.0:
        print "It's just enough to survive a trivial attack, but probably not a full offline cracking." 
        print "Try adding a service-specific string anywhere in it, this way your password becomes longer, and also unique among the sites you log in." 
        print "Also, have you considered using a password manager?"
    elif total < 94.0:
        print "Congrats, your password is long enough and should survive most offline, indirect cracking attacks, save perhaps for a very advanced actor."
        print "Be careful not to include things like dictionary words or easy repetitions in it. These make guessing it much easier than it seems!"
    elif total >= 94.0:
        print "Congrats, your password is long enough and should survive most offline, indirect cracking attacks, perhaps even by a very advanced actor..."
        print "... unless, of course, you cheated by using things like dictionary words, repetitions, easy-to-guess numbers, etc. that makes brute-forcing much easier than you might think. (Also, how can you memorize this?)"

if __name__ == "__main__":
    if "-t" in sys.argv:
        test_ent()
        sys.exit(0)
    main()

